# coding: utf-8

# -------------------------------------------------------------------------------
# Name:         dingtalk_client.py
# Description:  钉钉客户端
# Author:       XiangjunZhao
# EMAIL:        2419352654@qq.com
# Date:         2020/8/3 11:05
# -------------------------------------------------------------------------------
import base64
import hashlib
import hmac
import json
import logging
import queue
import re
import time
from json.decoder import JSONDecodeError
from urllib.parse import quote_plus
import requests

logger = logging.getLogger(__name__)


class DingTalkClient(object):
    """
    钉钉客户端
    """

    def __init__(self, webhook=None, secret=None, pc_slide=False, fail_notice=False):
        """
        初始化钉钉客户端
        Args:
            webhook: 钉钉webhook
            secret: 钉钉secret
            pc_slide: 消息链接打开方式，默认False为浏览器打开，设置为True时为PC客户端侧边栏打开
            fail_notice: 消息发送失败提醒，默认为False不提醒
        """
        self.headers = {'Content-Type': 'application/json; charset=utf-8'}
        self.queue = queue.Queue(20)  # 钉钉官方限流每分钟发送20条信息
        self.webhook = webhook
        self.secret = secret
        self.pc_slide = pc_slide
        self.fail_notice = fail_notice
        self.start_time = time.time()  # 加签时，请求时间戳与请求时间不能超过1小时，用于定时更新签名
        if self.secret and self.secret.startswith('SEC'):
            self.update_webhook()

    def update_webhook(self):
        """
        钉钉群自定义机器人安全设置加签时，签名中的时间戳与请求时不能超过一个小时，所以每个1小时需要更新签名
        Returns:

        """
        timestamp = str(round(self.start_time * 1000))
        string_to_sign = '{}\n{}'.format(timestamp, self.secret)
        hmac_code = hmac.new(self.secret.encode('utf-8'), string_to_sign.encode('utf-8'),
                             digestmod=hashlib.sha256).digest()
        sign = quote_plus(base64.b64encode(hmac_code))
        self.webhook = '{}&timestamp={}&sign={}'.format(self.webhook, timestamp, sign)

    def open_msg_type(self, url):
        """
        消息链接打开方式
        Args:
            url: 消息链接

        Returns:

        """
        pc_slide = 'true' if self.pc_slide else 'false'
        encoded_url = quote_plus(url)
        final_link = 'dingtalk://dingtalkclient/page/link?url={url}&pc_slide={pc_slide}'.format(url=encoded_url,
                                                                                                pc_slide=pc_slide)
        return final_link

    def post(self, data):
        """
        发送消息（内容UTF-8编码）
        Args:
            data: 消息数据（字典）

        Returns: 返回消息发送结果

        """
        now = time.time()

        # 钉钉自定义机器人安全设置加签时，签名中的时间戳与请求时不能超过一个小时，所以每个1小时需要更新签名
        if now - self.start_time >= 3600 and self.secret is not None and self.secret.startswith('SEC'):
            self.start_time = now
            self.update_webhook()

        # 钉钉自定义机器人现在每分钟最多发送20条消息
        self.queue.put(now)
        if self.queue.full():
            elapse_time = now - self.queue.get()
            if elapse_time < 60:
                sleep_time = int(60 - elapse_time) + 1
                logger.info('钉钉官方限制机器人每分钟最多发送20条，当前发送频率已达限制条件，休眠 {} s'.format(sleep_time))
                time.sleep(sleep_time)

        try:
            post_data = json.dumps(data)
            response = requests.post(self.webhook, headers=self.headers, data=post_data)
        except requests.exceptions.HTTPError as exc:
            logger.error("消息发送失败，HTTP error: %d，reason: %s" % (exc.response.status_code, exc.response.reason))
            raise
        except requests.exceptions.ConnectionError:
            logger.error("消息发送失败，HTTP connection error!")
            raise
        except requests.exceptions.Timeout:
            logger.error("消息发送失败，Timeout error!")
            raise
        except requests.exceptions.RequestException:
            logger.error("消息发送失败, Request Exception!")
            raise
        else:
            try:
                result = response.json()
            except JSONDecodeError:
                logger.error("服务器响应异常，状态码：{status_code}，响应内容：{text}".format(status_code=response.status_code,
                                                                            text=response.text))
                return {'errcode': 500, 'errmsg': '服务器响应异常'}
            else:
                logger.info('钉钉机器人发送消息结果：{}'.format(result))
                # 消息发送失败提醒（errcode 不为 0，表示消息发送异常）
                if self.fail_notice and result.get('errcode', True):
                    time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
                    error_data = {
                        "msgtype": "text",
                        "text": {
                            "content": "[注意-自动通知]钉钉机器人消息发送失败，时间：%s，原因：%s，请及时跟进，谢谢!" % (
                                time_now, result['errmsg'] if result.get('errmsg', False) else '未知异常')
                        },
                        "at": {
                            "isAtAll": False
                        }
                    }
                    logger.error("消息发送失败，自动通知：%s" % error_data)
                    requests.post(self.webhook, headers=self.headers, data=json.dumps(error_data))
            return result

    def send_text(self, msg, is_at_all=False, at_mobiles=[], is_auto_at=False):
        """
        发送文本类型消息
        Args:
            msg: 消息内容
            is_at_all: 是否@所有人
            at_mobiles: 被@人的手机号
            is_auto_at: 是否自动在msg内容末尾添加@手机号

        Returns: 返回消息发送结果

        """
        data = {'msgtype': 'text', 'at': {}}
        if msg:
            data['text'] = {'content': msg}
        else:
            logger.error('发送文本类型消息时，消息内容不能为空！！！')
            raise ValueError('发送文本类型消息时，消息内容不能为空！！！')

        if is_at_all:
            data['at']['isAtAll'] = is_auto_at

        if at_mobiles:
            data['at']['atMobiles'] = list(map(str, list(map(str, at_mobiles))))
            if is_auto_at:
                data['text']['content'] = msg + '\n@' + '@'.join(at_mobiles)
        logger.info('发送文本消息：{}'.format(data))
        return self.post(data)

    def send_image(self, pic_url):
        """
        发送图片类型消息（表情）
        Args:
            pic_url: 图片链接

        Returns: 返回消息发送结果

        """
        if pic_url:
            data = {
                'msgtype': 'image',
                'image': {
                    'picURL': pic_url
                }
            }
            logger.info('发送图片消息：{}'.format(data))
            return self.post(data)
        else:
            logger.error('发送图片类型消息时，图片链接不能为空！！！')
            raise ValueError('发送图片类型消息时，图片链接不能为空！！！')

    def send_markdown(self, title, text, is_at_all=False, at_mobiles=[], is_auto_at=False):
        """
        发送markdown类型消息
        Args:
            title: 首屏会话透出的展示内容
            text: markdown格式的消息内容
            is_at_all: 是否@所有人
            at_mobiles: 被@人的手机号
            is_auto_at: 是否自动在msg内容末尾添加@手机号

        Returns: 返回消息发送结果

        """
        if all([title, text]):
            text = re.sub(r'(?<!!)\[.*?\]\((.*?)\)',
                          lambda m: m.group(0).replace(m.group(1), self.open_msg_type(m.group(1))), text)
            data = {
                "msgtype": "markdown",
                "markdown": {
                    "title": title,
                    "text": text
                },
                "at": {}
            }

            if is_at_all:
                data['at']['isAtAll'] = is_auto_at

            if at_mobiles:
                data['at']['atMobiles'] = list(map(str, list(map(str, at_mobiles))))
                if is_auto_at:
                    data['text']['content'] = text + '\n@' + '@'.join(at_mobiles)

            logger.info('发送MarkDown消息：{}'.format(data))
        else:
            logger.error('发送图片类型消息时，消息标题或内容不能为空！！！')
            raise ValueError('发送图片类型消息时，消息标题或内容不能为空！！！')

    def send_link(self, title, text, message_url, pic_url=''):
        """
        发送链接类型消息
        Args:
            title: 消息标题
            text: 消息内容（如果太长自动省略显示）
            message_url: 点击消息触发的URL
            pic_url: 图片URL（可选）

        Returns: 返回消息发送结果

        """
        if all([title, text, message_url]):
            data = {
                "msgtype": "link",
                "link": {
                    "text": text,
                    "title": title,
                    "picUrl": pic_url,
                    "messageUrl": self.open_msg_type(message_url)
                }
            }
            logger.info('发送链接消息：{}'.format(data))
            return self.post(data)
        else:
            logger.error('发送链接类型消息时，消息标题或内容或链接不能为空！！！')
            raise ValueError('发送链接类型消息时，消息标题或内容或链接不能为空！！！')


if __name__ == '__main__':
    pass
